﻿/**
 * @copyright Copyright 2009 Structured Solutions
 * @license http://www.codeplex.com/ShippingAgent/license Microsoft Public License (Ms-PL)
 * @version 3.6
 */

using System;
using System.Collections;
using System.Collections.Generic;
using System.Data;
using System.Web.Services;
using System.Web.Services.Protocols;
using StoreFront.BusinessRule;
using StoreFront.BusinessRule.Management;
using StoreFront.BusinessRule.Orders;
using StoreFront.DataAccess;
using StoreFront.SystemBase;
using StoreFrontSecurity;
using StructuredSolutions.ShippingAgent.Properties;

namespace StructuredSolutions.ShippingAgent
{
    [WebService(Namespace = "http://www.codeplex.com/ShippingAgent",
        Description = "Shipping Agent for StoreFront 7.2")]
    public class ShippingAgentImpl : ShippingAgentService
    {
        private const string COD = "COD";
        private const string DISCOUNTS = "Discounts";
        private const string GIFT_CERTIFICATES = "Gift Certificates";
        private const string GIFTWRAPS = "Gift Wraps";
        private const string HANDLING = "Handling";
        private const string SHIPPING = "Shipping";
        private const string ORDER_STATUS_PENDING = "Shipment Pending";
        private const string ORDER_STATUS_SHIPPED = "Shipped";
        private const string PAYMENT_STATUS_PAID = "Paid";
        private const string PAYMENT_STATUS_PENDING = "Payment Pending";
        private const string TAXES = "Taxes";

        public const string VERSION = "3.6.0";

        #region WebMethod Implementations

        [WebMethod(Description = "Retrieve the features supported by this implementation of Shipping Agent.")]
        public override Features GetFeatures()
        {
            try
            {
                return new Features { SupportsNewAndUpdatedOrders = false };
            }
            catch (ApplicationException ex)
            {
                throw new DetailedSoapException(ex.Message, SoapException.ServerFaultCode, ex);
            }
            catch (Exception ex)
            {
                throw new DetailedSoapException("Cannot retrieve features.", SoapException.ServerFaultCode, ex);
            }
        }

        [WebMethod(Description = "Retrieve new and updated orders placed since pageKey.")]
        public override OrderPage GetNewAndUpdatedOrders(string orderStatusList, string paymentStatusList, string pageKey, int maxCount)
        {
            throw new DetailedSoapException("Not supported", SoapException.ServerFaultCode, new ApplicationException("Not supported."));
        }

        /// <summary>
        /// Return orders placed since pageKey.
        /// </summary>
        /// <param name="orderStatusList"></param>
        /// <param name="paymentStatusList"></param>
        /// <param name="pageKey">The last order number in the previous results. 
        /// If pageKey is null or an empty string, return all orders from today.</param>
        /// <param name="maxCount">The maximum number of orders to return.</param>
        /// <returns>Up to maxCount order IDs or an empty list or null 
        /// if there are no new orders.</returns>
        /// <remarks>
        /// If maxCount orders are returned, the Shipper client will call this 
        /// method again with the  same ranges and with pageKey set to the last order 
        /// number in the previous result. In this case, use pageKey to calculate an 
        /// appropriate new startOrderNumber (for example, startOrderNumber = pageKey + 1).
        /// </remarks>
        [WebMethod(Description = "Retrieve new orders.")]
        public override OrderPage GetNewOrders(string orderStatusList, string paymentStatusList, string pageKey, int maxCount)
        {
            try
            {
                return GetNewOrdersFromStore(orderStatusList, paymentStatusList, pageKey, maxCount);
            }
            catch (ApplicationException ex)
            {
                throw new DetailedSoapException(ex.Message, SoapException.ServerFaultCode, ex);
            }
            catch (Exception ex)
            {
                throw new DetailedSoapException("Cannot retrieve new orders.", SoapException.ServerFaultCode, ex);
            }
        }

        /// <summary>
        /// Return one order.
        /// </summary>
        /// <param name="orderId">The ID of the order.</param>
        /// <returns>The OrderResult.</returns>
        [WebMethod(Description = "Retrieve one order.")]
        public override OrderResult GetOrder(string orderId)
        {
            try
            {
                if (string.IsNullOrEmpty(orderId))
                    throw new ApplicationException("Invalid order ID.");
                return GetOrderFromStore(orderId);
            }
            catch (ApplicationException ex)
            {
                OrderResult result = new OrderResult();
                result.Success = false;
                result.Message = ex.Message;
                return result;
            }
            catch (Exception ex)
            {
                OrderResult result = new OrderResult();
                result.Success = false;
                result.Message = ex.ToString();
                return result;
            }
        }

        /// <summary>
        /// Return all the orders that fall on and within the specified ranges.
        /// </summary>
        /// <param name="startOrderNumber">The starting order number; or null or 
        /// an empty string to start at the first order number in the store.</param>
        /// <param name="endOrderNumber">The ending order number; or null or an 
        /// empty string to end with the last order number in the store.</param>
        /// <param name="startOrderDate">The starting order date. If no start date 
        /// is specified, start from the earliest order.</param>
        /// <param name="startOrderDateSpecified">True if startOrderDate is specified, 
        /// false otherwise.</param>
        /// <param name="endOrderDate">The ending order date. If no end date is specified, 
        /// end with the most recent order.</param>
        /// <param name="endOrderDateSpecified">True if endOrderDate is specified, 
        /// false otherwise.</param>
        /// <param name="paymentStatusList"></param>
        /// <param name="pageKey">The last order number from the previous result; 
        /// or null or an empty string to ignore this parameter.</param>
        /// <param name="maxCount">The maximum number of orders to return.</param>
        /// <param name="orderStatusList"></param>
        /// <returns>Up to maxCount order IDs; or an empty list or null if there are no 
        /// orders that fall on and within the specified ranges.</returns>
        /// <remarks>If maxCount orders are returned, the Shipper client will call this 
        /// method again with the same ranges and with lastOrderNumber set to the last 
        /// order number in the previous result. In this case, use lastOrderNumber to 
        /// calculate an appropriate new startOrderNumber (for example, 
        /// startOrderNumber = lastOrderNumber + 1).
        /// </remarks>
        [WebMethod(Description = "Return the list of orders that fall on and within the specified ranges.")]
        public override OrderPage GetOrdersByRange(string startOrderNumber, string endOrderNumber,
                                                   DateTime startOrderDate, bool startOrderDateSpecified,
                                                   DateTime endOrderDate, bool endOrderDateSpecified,
                                                   string orderStatusList, string paymentStatusList,
                                                   string pageKey, int maxCount)
        {
            try
            {
                DateTime? nullableStartDate = ShippingAgentHelper.ConvertWebServiceNullable(startOrderDate, startOrderDateSpecified);
                DateTime? nullableEndDate = ShippingAgentHelper.ConvertWebServiceNullable(endOrderDate, endOrderDateSpecified);
                return GetOrdersByRangeFromStore(startOrderNumber, endOrderNumber,
                	nullableStartDate, nullableEndDate, 
                    orderStatusList, paymentStatusList,
                    pageKey, maxCount);
            }
            catch (ApplicationException ex)
            {
                throw new DetailedSoapException(ex.Message, SoapException.ServerFaultCode, ex);
            }
            catch (Exception ex)
            {
                throw new DetailedSoapException("Cannot retrieve orders by range.", SoapException.ServerFaultCode, ex);
            }
        }

        [WebMethod(Description = "Retrieve the list of searchable order statuses.")]
        public override OrderStatus[] GetOrderStatuses()
        {
            return new []
                       {
                           new OrderStatus { Name = ORDER_STATUS_PENDING, OrderStatusId = ORDER_STATUS_PENDING },
                           new OrderStatus { Name = ORDER_STATUS_SHIPPED, OrderStatusId = ORDER_STATUS_SHIPPED }
                       };
        }

        /// <summary>
        /// Return one specific package.
        /// </summary>
        /// <param name="orderId">The ID of the order.</param>
        /// <param name="packageId">The ID of the package.</param>
        /// <returns>A PackageResult.</returns>
        [WebMethod(Description = "Retrieve one package.")]
        public override PackageResult GetPackage(string orderId, string packageId)
        {
            try
            {
                if (string.IsNullOrEmpty(orderId))
                    throw new ApplicationException("Invalid order ID.");
                if (string.IsNullOrEmpty(packageId))
                    throw new ApplicationException("Invalid package ID.");
                return GetPackageFromStore(orderId, packageId);
            }
            catch (ApplicationException ex)
            {
                PackageResult result = new PackageResult();
                result.Success = false;
                result.Message = ex.Message;
                return result;
            }
            catch (Exception ex)
            {
                PackageResult result = new PackageResult();
                result.Success = false;
                result.Message = ex.ToString();
                return result;
            }
        }

        [WebMethod(Description = "Retrieve the list of searchable order payment statuses.")]
        public override PaymentStatus[] GetPaymentStatuses()
        {
            return new []
                       {
                           new PaymentStatus { Name = PAYMENT_STATUS_PENDING, PaymentStatusId = PAYMENT_STATUS_PENDING }, 
                           new PaymentStatus { Name = PAYMENT_STATUS_PAID, PaymentStatusId = PAYMENT_STATUS_PAID }
                       };
        }

        /// <summary>
        /// Returns the complete list of shipping rate providers offered by the store.
        /// These are used to create criteria based on the rate provider or rate
        /// option.
        /// </summary>
        /// <returns>The list of RateProviders's.</returns>
        [WebMethod(Description = "Retrieve the list of shipping rate providers.")]
        public override RateProvider[] GetRateProviders()
        {
            try
            {
                return GetRateProvidersFromStore();
            }
            catch (ApplicationException ex)
            {
                throw new DetailedSoapException(ex.Message, SoapException.ServerFaultCode, ex);
            }
            catch (Exception ex)
            {
                throw new DetailedSoapException("Cannot retrieve rate providers.", SoapException.ServerFaultCode, ex);
            }
        }

        /// <summary>
        /// Returns the complete list of tracking providers supported by the store. The
        /// Shipper client may assign any tracking provider to a package, regardless of
        /// how the shipping cost was initially calculated.
        /// </summary>
        /// <returns>The list of TrackingProvider's.</returns>
        [WebMethod(Description = "Retrieve the list of package tracking providers.")]
        public override TrackingProvider[] GetTrackingProviders()
        {
            try
            {
                return GetTrackingProvidersFromStore();
            }
            catch (ApplicationException ex)
            {
                throw new DetailedSoapException(ex.Message, SoapException.ServerFaultCode, ex);
            }
            catch (Exception ex)
            {
                throw new DetailedSoapException("Cannot retrieve tracking providers.", SoapException.ServerFaultCode, ex);
            }
        }

        /// <summary>
        /// Return the ShippingAgent.wsdl implemented by this ShippingAgent.
        /// </summary>
        /// <returns>A string value that represents the version using the format
        /// Major.Minor.Revision. For example, "3.0.0".</returns>
        [WebMethod(Description = "Retrieve the WSDL interface version implemented by this Shipping Agent.")]
        public override string GetVersion()
        {
            return VERSION;
        }

        /// <summary>
        /// Record package information.
        /// </summary>
        /// <param name="update">The package information to record.</param>
        /// <returns>If the update is successful, the UpdateResult contains a fresh
        /// copy of the Order that contains the package. If the update is not
        /// successful, the UpdateResult contains the reason why the update failed.</returns>
        /// <remarks>The Shipper client may not include all Update elements. null
        /// elements should be ignored. If the ShipDate is included, then the package
        /// should be marked shipped.</remarks>
        [WebMethod(Description = "Record package information.")]
        public override UpdateResult UpdatePackage(Update update)
        {
            try
            {
                if (update == null)
                    return null;
                if (string.IsNullOrEmpty(update.OrderId))
                    throw new ApplicationException("Invalid order ID.");
                if (string.IsNullOrEmpty(update.PackageId))
                    throw new ApplicationException("Invalid package ID.");
                return UpdatePackageInStore(update);
            }
            catch (ApplicationException ex)
            {
                UpdateResult result = new UpdateResult();
                result.Success = false;
                result.Message = ex.Message;
                return result;
            }
            catch (Exception ex)
            {
                UpdateResult result = new UpdateResult();
                result.Success = false;
                result.Message = ex.ToString();
                return result;
            }
        }

        #endregion

        #region Platform Specific Methods

        protected static OrderPage GetNewOrdersFromStore(string orderStatusList, string paymentStatusList, string pageKey, int maxCount)
        {
            long sfLastOrderNumber = 0;
            if (!string.IsNullOrEmpty(pageKey))
            {
                if (!long.TryParse(pageKey, out sfLastOrderNumber))
                    throw new ApplicationException("Invalid paging key.");
            }

            int recordsCount;
            List<string> orderIds = new List<string>();

            using (COrderAccess sfOrderAccess = new COrderAccess())
            using (COrderAddressAccess sfOrderAddressAccess = new COrderAddressAccess())
            {
                string whereSql = "SELECT o.OrderNumber FROM (" + sfOrderAccess.TableName + " AS o INNER JOIN " + sfOrderAddressAccess.TableName + " AS a ON a.OrderID = o.uid) WHERE (o.OrderNumber IS NOT NULL) AND (o.Void <> 1) AND (a.Type = 4)";
                if (!string.IsNullOrEmpty(pageKey))
                {
                    whereSql += " AND (o.OrderNumber > " + sfLastOrderNumber + ")";
                }
                else
                {
                    // No pageKey means this is the first call. Return today's orders.
                    switch (StoreFrontConfiguration.DataBase)
                    {
                        case DatabaseType.Access:
                            whereSql += " AND (CDate(o.DateOrdered) >= #" + DateTime.Today.ToShortDateString() + "#)";
                            break;
                        case DatabaseType.SqlServer:
                            whereSql += " AND (CONVERT(DATETIME, o.DateOrdered) >= '" + DateTime.Today.ToShortDateString() + "')";
                            break;
                    }
                }
                List<string> having = new List<string>();
                if (!string.IsNullOrEmpty(orderStatusList))
                {
                    List<string> clauses = new List<string>();
                    foreach (string status in orderStatusList.Split(new char[] { ',', ';' }, StringSplitOptions.RemoveEmptyEntries))
                    {
                        switch (status)
                        {
                            case ORDER_STATUS_PENDING:
                                clauses.Add("(SUM(a.Pending) > 0 OR SUM(a.BOPending) > 0)");
                                break;
                            case ORDER_STATUS_SHIPPED:
                                clauses.Add("(SUM(a.Pending) = 0 AND SUM(a.BOPending) = 0)");
                                break;
                        }
                    }
                    having.Add(string.Join(" OR ", clauses.ToArray()));
                }
                if (!string.IsNullOrEmpty(paymentStatusList))
                {
                    List<string> clauses = new List<string>();
                    foreach (string status in paymentStatusList.Split(new char[] { ',', ';' }, StringSplitOptions.RemoveEmptyEntries))
                    {
                        switch (status)
                        {
                            case PAYMENT_STATUS_PAID:
                                clauses.Add("(o.PaymentsPending = 0 AND o.BOPaymentsPending = 0)");
                                break;
                            case ORDER_STATUS_PENDING:
                                clauses.Add("(o.PaymentsPending = 1 OR o.BOPaymentsPending = 1)");
                                break;
                        }
                    }
                    having.Add(string.Join(" OR ", clauses.ToArray()));
                }
                whereSql += " GROUP BY o.OrderNumber, o.PaymentsPending, o.BOPaymentsPending";
                if (having.Count > 0)
                    whereSql += " HAVING" + string.Join(" AND ", having.ToArray());
                string tempTable = string.Concat("(", whereSql, ") AS T1");

                string selectSql = "SELECT TOP " + maxCount + " OrderNumber FROM " + tempTable;
                using (DataSet sfOrders = sfOrderAccess.SQLSelect(selectSql + " ORDER BY OrderNumber", null, 0, 0))
                {
                    foreach (DataRow row in sfOrders.Tables[0].Rows)
                    {
                        orderIds.Add(ShippingAgentHelper.ConvertToString(row["OrderNumber"]));
                    }
                }

                string countSql = "SELECT COUNT(*) FROM " + tempTable;
                using (DataSet sfCount = sfOrderAccess.SQLSelect(countSql, null, 0, 0))
                {
                    recordsCount = Convert.ToInt32(sfCount.Tables[0].Rows[0][0]);
                }
            }

            if (orderIds.Count == 0)
                return null;

            OrderPage result = new OrderPage();
            result.Key = orderIds[orderIds.Count - 1];
            result.Orders = orderIds.ToArray();
            result.Remaining = recordsCount - orderIds.Count;
            return result;
        }

        /// <summary>
        /// Return one Order from the store.
        /// </summary>
        /// <param name="orderId">The OrderNumber of the StoreFront COrder object.</param>
        /// <returns>The Order corresponding to the StoreFront COrder.</returns>
        protected static OrderResult GetOrderFromStore(string orderId)
        {
            long sfOrderNumber;
            if (!long.TryParse(orderId, out sfOrderNumber))
                throw new ApplicationException("Invalid order ID.");

            return BuildOrderResult(sfOrderNumber);
        }

        protected static OrderPage GetOrdersByRangeFromStore(string startOrderNumber, string endOrderNumber,
                                                             DateTime? startOrderDate, DateTime? endOrderDate,
                                                             string orderStatusList, string paymentStatusList, 
                                                             string pageKey, int maxCount)
        {
            long sfStartOrderNumber = 0;
            long sfEndOrderNumber = 0;
            long sfLastOrderNumber = 0;
            if (!string.IsNullOrEmpty(startOrderNumber))
            {
                if (!long.TryParse(startOrderNumber, out sfStartOrderNumber))
                    throw new ApplicationException("Invalid start order number.");
            }
            if (!string.IsNullOrEmpty(endOrderNumber))
            {
                if (!long.TryParse(endOrderNumber, out sfEndOrderNumber))
                    throw new ApplicationException("Invalid end order number.");
            }
            if (!string.IsNullOrEmpty(pageKey))
            {
                if (!long.TryParse(pageKey, out sfLastOrderNumber))
                    throw new ApplicationException("Invalid paging key.");
            }

            List<string> orderIds = new List<string>();
            int recordsCount;
            using (COrderAccess sfOrderAccess = new COrderAccess())
            using (COrderAddressAccess sfOrderAddressAccess = new COrderAddressAccess())
            {
                string whereSql = "SELECT o.OrderNumber FROM (" + sfOrderAccess.TableName + " AS o INNER JOIN " + sfOrderAddressAccess.TableName + " AS a ON a.OrderID = o.uid) WHERE (o.OrderNumber IS NOT NULL) AND (o.Void <> 1) AND (a.Type = 4)";
                if (!string.IsNullOrEmpty(startOrderNumber))
                    whereSql += " AND (o.OrderNumber >= " + sfStartOrderNumber + ")";
                if (!string.IsNullOrEmpty(endOrderNumber))
                    whereSql += " AND (o.OrderNumber <= " + sfEndOrderNumber + ")";
                if (!string.IsNullOrEmpty(pageKey))
                    whereSql += " AND (o.OrderNumber > " + sfLastOrderNumber + ")";
                if (startOrderDate.HasValue)
                {
                    switch (StoreFrontConfiguration.DataBase)
                    {
                        case DatabaseType.Access:
                            whereSql += " AND (CDate(o.DateOrdered) >= #" + startOrderDate.Value.ToShortDateString() + "#)";
                            break;
                        case DatabaseType.SqlServer:
                            whereSql += " AND (CONVERT(DATETIME, o.DateOrdered) >= '" + startOrderDate.Value.ToShortDateString() + "')";
                            break;
                    }
                }
                if (endOrderDate.HasValue)
                {
                    switch (StoreFrontConfiguration.DataBase)
                    {
                        case DatabaseType.Access:
                            whereSql += " AND (CDate(o.DateOrdered) <= #" + endOrderDate.Value.ToShortDateString() + "#)";
                            break;
                        case DatabaseType.SqlServer:
                            whereSql += " AND (CONVERT(DATETIME, o.DateOrdered) <= '" + endOrderDate.Value.ToShortDateString() + "')";
                            break;
                    }
                }
                List<string> having = new List<string>();
                if (!string.IsNullOrEmpty(orderStatusList))
                {
                    List<string> clauses = new List<string>();
                    foreach (string status in orderStatusList.Split(new char[] {',', ';'}, StringSplitOptions.RemoveEmptyEntries))
                    {
                        switch (status)
                        {
                            case ORDER_STATUS_PENDING:
                                clauses.Add("(SUM(a.Pending) > 0 OR SUM(a.BOPending) > 0)");
                                break;
                            case ORDER_STATUS_SHIPPED:
                                clauses.Add("(SUM(a.Pending) = 0 AND SUM(a.BOPending) = 0)");
                                break;
                        }
                    }
                    having.Add(string.Join(" OR ", clauses.ToArray()));
                }
                if (!string.IsNullOrEmpty(paymentStatusList))
                {
                    List<string> clauses = new List<string>();
                    foreach (string status in paymentStatusList.Split(new char[] { ',', ';' }, StringSplitOptions.RemoveEmptyEntries))
                    {
                        switch (status)
                        {
                            case PAYMENT_STATUS_PAID:
                                clauses.Add("(o.PaymentsPending = 0 AND o.BOPaymentsPending = 0)");
                                break;
                            case ORDER_STATUS_PENDING:
                                clauses.Add("(o.PaymentsPending = 1 OR o.BOPaymentsPending = 1)");
                                break;
                        }
                    }
                    having.Add(string.Join(" OR ", clauses.ToArray()));
                }
                whereSql += " GROUP BY o.OrderNumber, o.PaymentsPending, o.BOPaymentsPending";
                if (having.Count > 0)
                    whereSql += " HAVING" + string.Join(" AND ", having.ToArray());
                string tempTable = string.Concat("(", whereSql, ") AS T1");

                string selectSql = "SELECT TOP " + maxCount + " OrderNumber FROM " + tempTable;
                using (DataSet sfOrders = sfOrderAccess.SQLSelect(selectSql + " ORDER BY OrderNumber", null, 0, 0))
                {
                    foreach (DataRow row in sfOrders.Tables[0].Rows)
                    {
                        orderIds.Add(ShippingAgentHelper.ConvertToString(row["OrderNumber"]));
                    }
                }

                string countSql = "SELECT COUNT(*) FROM " + tempTable;
                using (DataSet sfCount = sfOrderAccess.SQLSelect(countSql, null, 0, 0))
                {
                    recordsCount = Convert.ToInt32(sfCount.Tables[0].Rows[0][0]);
                }
            }

            if (orderIds.Count == 0)
                return null;

            OrderPage result = new OrderPage();
            result.Key = orderIds[orderIds.Count - 1];
            result.Orders = orderIds.ToArray();
            result.Remaining = recordsCount - orderIds.Count;
            return result;
        }

        /// <summary>
        /// Return one Shipment from the store.
        /// </summary>
        /// <param name="orderId">The OrderNumber of the StoreFront COrder object.</param>
        /// <param name="packageId">The ID of the corresponding OrderAddress object and a flag
        /// to indicate the order ("a") or backorder shipment ("b").</param>
        /// <returns>The Shipment.</returns>
        /// <remarks>StoreFront stores shipments in objects called OrderAddress. Each
        /// OrderAddress can contain 1 or 2 shipments: the order shipment and the
        /// backorder shipment.</remarks>
        protected static PackageResult GetPackageFromStore(string orderId, string packageId)
        {
            long sfOrderNumber;
            if (!long.TryParse(orderId, out sfOrderNumber))
                throw new ApplicationException("Invalid order ID.");

            bool sfBackorder;
            if (packageId.StartsWith("a", StringComparison.InvariantCultureIgnoreCase))
                sfBackorder = false;
            else if (packageId.StartsWith("b", StringComparison.InvariantCultureIgnoreCase))
                sfBackorder = true;
            else
                throw new ApplicationException("Invalid package ID.");
            long sfOrderAddressId;

            if (!long.TryParse(packageId.Substring(1), out sfOrderAddressId))
                throw new ApplicationException("Invalid package ID.");

            using (COrder sfOrder = new COrder(sfOrderNumber))
            {
                foreach (COrderAddress sfOrderAddress in sfOrder.OrderAddresses)
                {
                    if (sfOrderAddress.Address.ID == sfOrderAddressId)
                        return BuildPackageResult(sfOrder, sfOrderAddress, sfBackorder);
                }
            }

            throw new ApplicationException("Invalid order or shipment ID.");
        }

        protected static RateProvider[] GetRateProvidersFromStore()
        {
            List<RateProvider> rateProviders = new List<RateProvider>();

            using (CShippingManagement management = new CShippingManagement())
            {
                switch (management.AdminShipping.ShippingType)
                {
                    case 1: // Value Based Shipping
                        {
                            List<string> rateOptions = new List<string>();
                            rateOptions.Add("By Value");
                            if (management.AdminShipping.ApplyPremium != 0)
                                rateOptions.Add(management.AdminShipping.PremiumName);
                            RateProvider rateProvider = new RateProvider();
							rateProvider.RateOptions = rateOptions.ToArray();
                            rateProviders.Add(rateProvider);
                            break;
                        }
                    case 2: // Carrier Based Shipping
                        using (DataSet sfCarriers = management.getCarriers())
                        {
                            foreach (DataRow sfCarrier in sfCarriers.Tables[0].Rows)
                            {
                                string sfCarrierName = ShippingAgentHelper.ConvertToString(sfCarrier["Name"]);
                                if (string.IsNullOrEmpty(sfCarrierName)) continue;

                                string sfCarrierCode = ShippingAgentHelper.ConvertToString(sfCarrier["Code"]);
                                if (string.IsNullOrEmpty(sfCarrierCode) || sfCarrierCode.Equals("none", StringComparison.InvariantCultureIgnoreCase))
                                    continue;

                                bool sfCarrierActive = ShippingAgentHelper.ConvertToBoolean(sfCarrier["Active"], false);
                                if (!sfCarrierActive) continue;

                                RateProvider rateProvider = new RateProvider();
                                rateProvider.Name = sfCarrierName;

                                List<string> rateOptions = new List<string>();
                                using (DataSet sfMethods = management.getCarrierMethods(sfCarrierCode))
                                {
                                    foreach (DataRow sfMethod in sfMethods.Tables[0].Rows)
                                    {
                                        string sfMethodName = ShippingAgentHelper.ConvertToString(sfMethod["Method"]);
                                        if (string.IsNullOrEmpty(sfMethodName)) continue;

                                        bool sfMethodActive = ShippingAgentHelper.ConvertToBoolean(sfMethod["IsActive"], false);
                                        if (!sfMethodActive) continue;

                                        rateOptions.Add(sfMethodName);
                                    }
                                }
                                rateProvider.RateOptions = rateOptions.ToArray();
                                rateProviders.Add(rateProvider);
                            }
                        }
                        break;
                    case 3: // Product Based Shipping
                        {
                            List<string> rateOptions = new List<string>();
                            rateOptions.Add("By Product");
                            if (management.AdminShipping.ApplyPremium != 0)
                                rateOptions.Add(management.AdminShipping.PremiumName);
                            RateProvider rateProvider = new RateProvider();
                            rateProvider.RateOptions = rateOptions.ToArray();
                            rateProviders.Add(rateProvider);
                            break;
                        }
                }
            }

            return rateProviders.ToArray();
        }

        protected static TrackingProvider[] GetTrackingProvidersFromStore()
        {
            List<TrackingProvider> trackingProviders = new List<TrackingProvider>();

            using (CShippingManagement management = new CShippingManagement())
            using (DataSet sfCarriers = management.getCarriers())
            {
                foreach (DataRow sfCarrier in sfCarriers.Tables[0].Rows)
                {
                    string sfCarrierName = ShippingAgentHelper.ConvertToString(sfCarrier["Name"]);
                    if (string.IsNullOrEmpty(sfCarrierName)) continue;

                    string sfCarrierCode = ShippingAgentHelper.ConvertToString(sfCarrier["Code"]);
                    if (string.IsNullOrEmpty(sfCarrierCode) || sfCarrierCode.Equals("none", StringComparison.InvariantCultureIgnoreCase)) continue;

                    TrackingProvider provider = new TrackingProvider();
                    provider.Name = sfCarrierName;
                    provider.TrackingProviderId = sfCarrierCode;
                    trackingProviders.Add(provider);
                }
            }

            return trackingProviders.ToArray();
        }

        protected static UpdateResult UpdatePackageInStore(Update update)
        {
            long sfOrderNumber;
            if (!long.TryParse(update.OrderId, out sfOrderNumber))
                throw new ApplicationException("Invalid order ID.");

            bool sfBackorder;
            if (update.PackageId.StartsWith("a", StringComparison.InvariantCultureIgnoreCase))
                sfBackorder = false;
            else if (update.PackageId.StartsWith("b", StringComparison.InvariantCultureIgnoreCase))
                sfBackorder = true;
            else
                throw new ApplicationException("Invalid package ID.");
            long sfOrderAddressId;

            if (!long.TryParse(update.PackageId.Substring(1), out sfOrderAddressId))
                throw new ApplicationException("Invalid package ID.");

            using (COrder sfOrder = new COrder(sfOrderNumber))
            {
                foreach (COrderAddress sfOrderAddress in sfOrder.OrderAddresses)
                {
                    if (sfOrderAddress.Address.ID == sfOrderAddressId)
                        return BuildUpdateResult(update, sfOrder, sfOrderAddress, sfBackorder);
                }
            }
            throw new ApplicationException("Invalid order and shipment ID.");
        }

        #endregion

        private static bool AddressesAreEqual(COrderAddress sfOrderAddress, ShippingAddress shippingAddress)
        {
            if (!ShippingAgentHelper.StringsAreEqual(sfOrderAddress.Address.City, shippingAddress.City, StringComparison.CurrentCulture)) return false;
            if (!ShippingAgentHelper.StringsAreEqual(sfOrderAddress.Address.Company, shippingAddress.Company, StringComparison.CurrentCulture)) return false;
            if (!ShippingAgentHelper.StringsAreEqual(sfOrderAddress.Address.Country, shippingAddress.Country, StringComparison.InvariantCultureIgnoreCase)) return false;
            if (!ShippingAgentHelper.StringsAreEqual(sfOrderAddress.Address.Fax, shippingAddress.Fax, StringComparison.CurrentCulture)) return false;
            if (!ShippingAgentHelper.StringsAreEqual(sfOrderAddress.Address.FirstName, shippingAddress.FirstName, StringComparison.CurrentCulture)) return false;
            if (!ShippingAgentHelper.StringsAreEqual(sfOrderAddress.Address.LastName, shippingAddress.LastName, StringComparison.CurrentCulture)) return false;
            if (!ShippingAgentHelper.StringsAreEqual(sfOrderAddress.Address.Address1, shippingAddress.Line1, StringComparison.CurrentCulture)) return false;
            if (!ShippingAgentHelper.StringsAreEqual(sfOrderAddress.Address.Address2, shippingAddress.Line2, StringComparison.CurrentCulture)) return false;
            if (!ShippingAgentHelper.StringsAreEqual(sfOrderAddress.Address.Phone, shippingAddress.Phone, StringComparison.CurrentCulture)) return false;
            if (!ShippingAgentHelper.StringsAreEqual(sfOrderAddress.Address.Zip, shippingAddress.PostalCode, StringComparison.CurrentCulture)) return false;
            if (!ShippingAgentHelper.StringsAreEqual(sfOrderAddress.Address.State, shippingAddress.Region, StringComparison.InvariantCultureIgnoreCase)) return false;
            return sfOrderAddress.Residential == shippingAddress.Residential;
        }

        private static Address BuildBillingAddress(COrder sfOrder)
        {
            Address address = new Address();
            address.AddressId = sfOrder.BillAddress.ID.ToString();
            address.City = ShippingAgentHelper.StringOrNull(sfOrder.BillAddress.City);
            address.Company = ShippingAgentHelper.StringOrNull(sfOrder.BillAddress.Company);
            address.Country = ShippingAgentHelper.StringOrNull(sfOrder.BillAddress.Country);
            address.Email = ShippingAgentHelper.StringOrNull(sfOrder.BillAddress.EMail);
            address.Fax = ShippingAgentHelper.StringOrNull(sfOrder.BillAddress.Fax);
            address.FirstName = ShippingAgentHelper.StringOrNull(sfOrder.BillAddress.FirstName);
            address.LastName = ShippingAgentHelper.StringOrNull(sfOrder.BillAddress.LastName);
            address.Line1 = ShippingAgentHelper.StringOrNull(sfOrder.BillAddress.Address1);
            address.Line2 = ShippingAgentHelper.StringOrNull(sfOrder.BillAddress.Address2);
            address.Phone = ShippingAgentHelper.StringOrNull(sfOrder.BillAddress.Phone);
            address.PostalCode = ShippingAgentHelper.StringOrNull(sfOrder.BillAddress.Zip);
            address.Region = ShippingAgentHelper.StringOrNull(sfOrder.BillAddress.State);
            return address;
        }

        private static Customer BuildCustomer(COrder sfOrder)
        {
            Customer customer = new Customer();
            StoreFront.SystemBase.Customer sfCustomer = new StoreFront.SystemBase.Customer();
            new CCustomer().FillCustomer(ref sfCustomer, sfOrder.CustomerID);
            customer.CustomerId = sfOrder.CustomerID.ToString();
            customer.CustomerUrl = BuildCustomerUrl("CustProfileMain.aspx", "");
            customer.Email = sfCustomer.Email;
            customer.FirstName = sfCustomer.FirstName;
            customer.LastName = sfCustomer.LastName;
            customer.MerchantUrl = BuildMerchantUrl("management/Customers.aspx", "");
            customer.UserName = sfCustomer.Email;
            return customer;
        }

        private static string BuildCustomerUrl(string page, string query)
        {
            try
            {
                UriBuilder url = new UriBuilder(StoreFrontConfiguration.SiteURL);
                url.Path += page;
                url.Query = query;
                return url.Uri.ToString();
            }
            catch
            {
                return string.Empty;
            }
        }

        private static decimal BuildInsuranceValue(ArrayList sfItems, bool sfBackorder)
        {
            decimal value = 0;
            foreach (COrderItem sfItem in sfItems)
            {
                if (!sfItem.IsShipable) continue;

                long backorderQuantity = sfItem.SavedBOQty;
                long orderQuantity = sfItem.Quantity - backorderQuantity;

                if (sfBackorder && backorderQuantity <= 0) continue;
                if (!sfBackorder && orderQuantity <= 0) continue;

                long quantity = sfBackorder ? backorderQuantity : orderQuantity;

                switch (Settings.Default.InsuranceValue.Replace(" ", string.Empty).ToUpperInvariant())
                {
                    case "COST":
                        value += quantity * sfItem.Cost;
                        break;
                    default:
                        value += quantity * sfItem.ItemPrice;
                        break;
                }
            }
            return value;
        }

        private static Property[] BuildItemProperties(COrderItem sfOrderItem)
        {
            List<Property> properties = new List<Property>();
            foreach (CAttribute attribute in sfOrderItem.Attributes)
            {
                Property property = new Property();
                property.Name = attribute.Name;

                List<string> details = new List<string>();
                foreach (CAttributeDetail detail in attribute.AttributeDetails)
                {
                    if (attribute.AttributeType == tAttributeType.Normal)
                        details.Add(detail.Name);
                    else
                        details.Add(detail.Customor_Custom_Description);
                }

                property.Value = string.Join(", ", details.ToArray());
                properties.Add(property);
            }
            foreach (CGiftWrap wrap in sfOrderItem.GiftWraps)
            {
                Property property = new Property();
                property.Name = "Gift Wrap";
                property.Value = string.Format("FROM: {0} TO: {1} MESSAGE: {2}", wrap.MessageFrom, wrap.MessageTo, wrap.Message);
                properties.Add(property);
            }
            return properties.Count == 0 ? null : properties.ToArray();
        }

        private static Supplier BuildManufacturer(IProduct sfItem)
        {
            // StoreFront does not fill in the manufacturer
            try
            {
                using (CProductManagement product = new CProductManagement(Convert.ToInt32(sfItem.ProductID)))
                using (CManufacturer manufacturer = new CManufacturer(product.Manufacturer))
                {
                    if (manufacturer.ManufacturerAddress.Company.Equals("no manufacturer", StringComparison.CurrentCultureIgnoreCase))
                        return null;

                    Supplier supplier = new Supplier();
                    supplier.Email = ShippingAgentHelper.StringOrNull(manufacturer.ManufacturerAddress.EMail);
                    supplier.Name = manufacturer.ManufacturerAddress.Company;
                    supplier.SupplierId = product.Manufacturer.ToString();
                    return supplier;
                }
            }
            catch (IndexOutOfRangeException)
            {
                // Product not in database
                return null;
            }
        }

        private static string BuildMerchantUrl(string page, string query)
        {
            try
            {
                UriBuilder url = new UriBuilder(StoreFrontConfiguration.SSLPath);
                url.Path += page;
                url.Query = query;
                return url.Uri.ToString();
            }
            catch
            {
                return string.Empty;
            }
        }

        private static NamedAmount[] BuildOrderCharges(ITotalTax sfOrder)
        {
            NamedAmountList charges = new NamedAmountList();
            if (sfOrder.CODTotal != 0) charges[COD].Amount += sfOrder.CODTotal;
            if (sfOrder.CountryTax != 0) charges[TAXES].Amount += sfOrder.CountryTax;
            if (sfOrder.HandlingTotal != 0) charges[HANDLING].Amount += sfOrder.HandlingTotal;
            if (sfOrder.LocalTax != 0) charges[TAXES].Amount += sfOrder.LocalTax;
            if (sfOrder.ShippingTotal != 0) charges[SHIPPING].Amount += sfOrder.ShippingTotal;
            if (sfOrder.StateTax != 0) charges[TAXES].Amount += sfOrder.StateTax;
            return charges.Count > 0 ? charges.ToArray() : null;
        }

        private static ItemResult BuildOrderItemResult(COrderItem sfOrderItem)
        {
            ItemResult itemResult = new ItemResult();
            Item item = new Item();
            itemResult.Item = item;

            item.ItemId = sfOrderItem.OrderItemID.ToString();
            item.LineTotal = sfOrderItem.ItemTotal - sfOrderItem.GiftWrapPrice;
            item.Name = sfOrderItem.Name;
            item.Price = sfOrderItem.ItemPrice;
            item.Quantity = sfOrderItem.Quantity;
            item.Shippable = sfOrderItem.IsShipable;
            item.Weight = item.Quantity * Convert.ToDouble(sfOrderItem.Weight);

            try
            {
                item.Code = sfOrderItem.GetSku();
                if (string.IsNullOrEmpty(item.Code))
                {
                    item.Code = sfOrderItem.ProductCode;
                }
                if (sfOrderItem.Inventory != null && sfOrderItem.Inventory.InventoryTracked)
                {
                    CAttributes attributes = new CAttributes();
                    foreach (CAttribute attribute in sfOrderItem.Attributes)
                        attributes.Add(attribute);
                    item.Inventory = sfOrderItem.Inventory.GetQuantity(attributes);
                }
                item.Manufacturer = BuildManufacturer(sfOrderItem);
                item.Properties = BuildItemProperties(sfOrderItem);
                item.Vendor = BuildVendor(sfOrderItem);

                itemResult.Success = true;
            }
            catch (Exception ex)
            {
                itemResult.Success = false;
                itemResult.Message = ex.ToString();
            }

            return itemResult;
        }

        private static ItemResult[] BuildOrderItemResults(COrder sfOrder)
        {
            List<ItemResult> itemResults = new List<ItemResult>();

            foreach (COrderAddress sfOrderAddress in sfOrder.OrderAddresses)
            {
                foreach (COrderItem sfOrderItem in sfOrderAddress.OrderItems)
                {
                    itemResults.Add(BuildOrderItemResult(sfOrderItem));
                }
            }

            return itemResults.Count == 0 ? null : itemResults.ToArray();
        }

        private static NamedAmount[] BuildOrderPayments(COrder sfOrder)
        {
            NamedAmountList payments = new NamedAmountList();
            if (sfOrder.GiftCertificateTotal != 0) payments[GIFT_CERTIFICATES].Amount += sfOrder.GiftCertificateTotal;
            if (sfOrder.DiscountTotal != 0) payments[DISCOUNTS].Amount += sfOrder.DiscountTotal;
            if (!sfOrder.PaymentsPending)
            {
                NamedAmount payment = new NamedAmount();
                payment.Amount = sfOrder.TotalBilledAmt;
                payment.Description = BuildPaymentDescription(sfOrder);
                payment.Name = "Order Payment";
                payments.Add(payment);
            }
            if (sfOrder.BackOrderTotalAmt > 0 && !sfOrder.BOPaymentsPending)
            {
                NamedAmount payment = new NamedAmount();
                payment.Amount = sfOrder.BackOrderTotalAmt;
                payment.Description = BuildPaymentDescription(sfOrder);
                payment.Name = "Backorder Payment";
                payments.Add(payment);
            }
            return payments.Count > 0 ? payments.ToArray() : null;
        }

        private static OrderResult BuildOrderResult(CReportDetails sfOrderSummary)
        {
            OrderResult orderResult = new OrderResult();
            Order order = new Order();
            orderResult.Order = order;

            order.MerchantUrl = BuildMerchantUrl("Management/OrderStatus.aspx", "OrderID=" + sfOrderSummary.OrderNumber);
            DateTime? orderDate = ShippingAgentHelper.ConvertToNullableDateTime(sfOrderSummary.OrderDate);
            order.OrderDate = orderDate.HasValue ? orderDate.Value : DateTime.MinValue;
            order.LastUpdated = order.OrderDate;
            order.OrderId = sfOrderSummary.OrderNumber.ToString();
            order.OrderNumber = sfOrderSummary.OrderNumber.ToString();

            try
            {
                using (COrder sfOrder = new COrder(sfOrderSummary.OrderNumber))
                {
                    order.CustomerUrl = BuildCustomerUrl("OrderDetail.aspx", "OrderID=" + sfOrder.UID);
                    order.OrderTotal = Convert.ToDecimal(sfOrder.GrandTotal);
                    order.BillingAddress = BuildBillingAddress(sfOrder);
                    order.Charges = BuildOrderCharges(sfOrder);
                    order.Customer = BuildCustomer(sfOrder);
                    order.Items = BuildOrderItemResults(sfOrder);
                    order.OrderStatus = BuildOrderStatus(sfOrder);
                    order.Packages = BuildPackageRefs(sfOrder);
                    order.Payments = BuildOrderPayments(sfOrder);
                    order.PaymentStatus = BuildPaymentStatus(sfOrder);
                    order.ShippingStatus = BuildShippingStatus(sfOrder);

                    orderResult.Success = true;
                }
            }
            catch (ApplicationException ex)
            {
                orderResult.Success = false;
                orderResult.Message = ex.Message;
            }
            catch (Exception ex)
            {
                orderResult.Success = false;
                orderResult.Message = ex.ToString();
            }
            return orderResult;
        }

        private static OrderResult BuildOrderResult(long sfOrderNumber)
        {
            sfSearchContainer search = new sfSearchContainer();
            search.OrderNumber = sfOrderNumber;
            using (CManagement management = new CManagement(search))
            {
                ArrayList summaries = management.OrderSummary;
                if (summaries.Count == 0)
                    throw new ApplicationException("Invalid order ID.");

                return BuildOrderResult((COrderManagmentSummary) summaries[0]);
            }
        }

        private static string BuildOrderStatus(COrder sfOrder)
        {
            using (CManagement management = new CManagement())
            {
                foreach (COrderShipmentStatus sfShipmentStatus in management.get_OrderShipStatus(sfOrder.UID))
                {
                    if (sfShipmentStatus.HasShipmentPending || sfShipmentStatus.HasBOShipmentPending)
                        return ORDER_STATUS_PENDING;
                }
            }
            return ORDER_STATUS_SHIPPED;
        }

        private static NamedAmount[] BuildPackageCharges(COrderAddress sfOrderAddress, bool sfBackorder)
        {
            NamedAmountList charges = new NamedAmountList();
            if (sfBackorder)
            {
                if (sfOrderAddress.BackOrderCountryTaxTotal != 0) charges[TAXES].Amount += sfOrderAddress.BackOrderCountryTaxTotal;
                if (sfOrderAddress.BackOrderHandlingTotal != 0) charges[HANDLING].Amount += sfOrderAddress.BackOrderHandlingTotal;
                if (sfOrderAddress.BackOrderLocalTaxTotal != 0) charges[TAXES].Amount += sfOrderAddress.BackOrderLocalTaxTotal;
                if (sfOrderAddress.BackOrderStateTaxTotal != 0) charges[TAXES].Amount += sfOrderAddress.BackOrderStateTaxTotal;
            }
            else
            {
                if (sfOrderAddress.NonBackOrderCountryTaxTotal != 0) charges[TAXES].Amount += sfOrderAddress.NonBackOrderCountryTaxTotal;
                if (sfOrderAddress.NonBackOrderHandlingTotal != 0) charges[HANDLING].Amount += sfOrderAddress.NonBackOrderHandlingTotal;
                if (sfOrderAddress.NonBackOrderLocalTaxTotal != 0) charges[TAXES].Amount += sfOrderAddress.NonBackOrderLocalTaxTotal;
                if (sfOrderAddress.NonBackOrderStateTaxTotal != 0) charges[TAXES].Amount += sfOrderAddress.NonBackOrderStateTaxTotal;
                if (sfOrderAddress.ShippingTotal != 0) charges[SHIPPING].Amount += sfOrderAddress.ShippingTotal;
                if (sfOrderAddress.DiscountTotal != 0) charges[DISCOUNTS].Amount += sfOrderAddress.DiscountTotal;
                // Move gift wrap charges from line item total to shipment charges
                foreach (COrderItem item in sfOrderAddress.OrderItems)
                {
                    if (item.GiftWrapPrice != 0) charges[GIFTWRAPS].Amount += item.GiftWrapPrice;
                }
            }
            return charges.Count > 0 ? charges.ToArray() : null;
        }

        private static ItemResult BuildPackageItemResult(COrderItem sfOrderItem, bool sfBackorder)
        {
            long backorderQuantity = sfOrderItem.SavedBOQty;
            long orderQuantity = sfOrderItem.Quantity - backorderQuantity;

            if (sfBackorder && backorderQuantity <= 0)
                return null;
            if (!sfBackorder && orderQuantity <= 0)
                return null;

            ItemResult itemResult = new ItemResult();
            Item item = new Item();
            itemResult.Item = item;

            item.ItemId = sfOrderItem.OrderItemID.ToString();
            item.LineTotal = sfBackorder ? sfOrderItem.BackOrderTotal : sfOrderItem.ItemTotal - sfOrderItem.BackOrderTotal - sfOrderItem.GiftWrapPrice;
            item.Name = sfOrderItem.Name;
            item.Price = sfOrderItem.ItemPrice;
            item.Quantity = sfBackorder ? backorderQuantity : orderQuantity;
            item.Shippable = sfOrderItem.IsShipable;
            item.Weight = item.Quantity * Convert.ToDouble(sfOrderItem.Weight);

            try
            {
                item.Code = sfOrderItem.GetSku();
                if (string.IsNullOrEmpty(item.Code))
                {
                    item.Code = sfOrderItem.ProductCode;
                }
                if (sfOrderItem.Inventory != null && sfOrderItem.Inventory.InventoryTracked)
                {
                    CAttributes attributes = new CAttributes();
                    foreach (CAttribute attribute in sfOrderItem.Attributes)
                        attributes.Add(attribute);
                    item.Inventory = sfOrderItem.Inventory.GetQuantity(attributes);
                }
                item.Manufacturer = BuildManufacturer(sfOrderItem);
                item.Properties = BuildItemProperties(sfOrderItem);
                item.Vendor = BuildVendor(sfOrderItem);

                itemResult.Success = true;
            }
            catch (Exception ex)
            {
                itemResult.Success = false;
                itemResult.Message = ex.ToString();
            }

            return itemResult;
        }

        private static ItemResult[] BuildPackageItemResults(COrderAddress sfOrderAddress, bool sfBackorder)
        {
            List<ItemResult> itemResults = new List<ItemResult>();

            foreach (COrderItem sfOrderItem in sfOrderAddress.OrderItems)
            {
                int backorderQuantity = sfOrderItem.SavedBOQty;
                long orderQuantity = sfOrderItem.Quantity - backorderQuantity;

                if (sfBackorder && backorderQuantity <= 0) continue;
                if (!sfBackorder && orderQuantity <= 0) continue;

                itemResults.Add(BuildPackageItemResult(sfOrderItem, sfBackorder));
            }

            return itemResults.Count == 0 ? null : itemResults.ToArray();
        }

        private static PackageResult BuildPackageResult(COrder sfOrder, COrderAddress sfOrderAddress, bool sfBackorder)
        {
            if (sfBackorder && !sfOrderAddress.HasBackOrderShippableProducts)
            {
				// A PackageResult with null Package tells Shipper the
				// package is no longer in the store.
                PackageResult result = new PackageResult();
                result.Success = true;
                return result;
            }
            if (!sfBackorder && !sfOrderAddress.HasNonBackOrderShippableProducts)
            {
                PackageResult result = new PackageResult();
                result.Success = true;
                return result;
            }

            PackageResult packageResult = new PackageResult();
            Package package = new Package();
            packageResult.Package = package;

            package.CustomerUrl = BuildCustomerUrl("OrderTracking.aspx", "OrderID=" + sfOrder.UID);
            package.MerchantUrl = BuildMerchantUrl("management/TrackShipment.aspx",
                string.Format("OrderID={0}&OrderAddressID={1}", sfOrder.UID, sfOrderAddress.Address.ID));
            package.OrderId = sfOrder.OrderNumber.ToString();
            package.PackageId = (sfBackorder ? "b" : "a") + sfOrderAddress.Address.ID;

            try
            {
                package.Charges = BuildPackageCharges(sfOrderAddress, sfBackorder);
                package.Destination = BuildShippingAddress(sfOrderAddress.Address, sfOrderAddress.Residential);
                package.Instructions = ShippingAgentHelper.StringOrNull(sfOrderAddress.Address.Instructions);
                package.InsuranceValue = BuildInsuranceValue(sfOrderAddress.OrderItems, sfBackorder);
                package.Items = BuildPackageItemResults(sfOrderAddress, sfBackorder);
                package.RateOptionName = sfOrderAddress.Address.ShipMethod;
                package.RateProviderName = BuildRateProviderName(sfOrderAddress.Address.ShipCarrierCode);
                DateTime? shipDate = BuildShipDate(sfOrder, sfOrderAddress, sfBackorder);
                if (shipDate.HasValue)
                {
                    package.ShipDate = shipDate.Value;
                    package.ShipDateSpecified = true;
                }
                else
                {
                    package.ShipDateSpecified = false;
                }
                package.Shipped = BuildShipped(sfOrder, sfOrderAddress, sfBackorder);
                package.TrackingNumber = BuildTrackingNumber(sfOrderAddress, sfBackorder);
                package.TrackingProviderId = ShippingAgentHelper.StringOrNull(sfOrderAddress.Address.ShipCarrierCode);
                package.TrackingProviderName = package.RateProviderName;
                package.Warehouse = BuildWarehouse();
                package.Weight = BuildPackageWeight(sfOrderAddress.OrderItems, sfBackorder);

                packageResult.Success = true;
            }
            catch (ApplicationException ex)
            {
                packageResult.Success = false;
                packageResult.Message = ex.Message;
            }
            catch (Exception ex)
            {
                packageResult.Success = false;
                packageResult.Message = ex.ToString();
            }
            return packageResult;
        }

        private static PackageRef[] BuildPackageRefs(COrder sfOrder)
        {
            List<PackageRef> packageRefs = new List<PackageRef>();
            foreach (COrderAddress sfOrderAddress in sfOrder.OrderAddresses)
            {
                if (sfOrderAddress.HasShippableProducts)
                {
                    PackageRef packageRef = new PackageRef();
                    packageRef.OrderId = sfOrder.OrderNumber.ToString();
                    if (sfOrderAddress.HasNonBackOrderShippableProducts)
                        packageRef.PackageId = "a" + sfOrderAddress.Address.ID;
                    if (sfOrderAddress.HasBackOrderShippableProducts)
                        packageRef.PackageId = "b" + sfOrderAddress.Address.ID;
                    packageRefs.Add(packageRef);
                }
            }
            return packageRefs.Count > 0 ? packageRefs.ToArray() : null;
        }

        private static double BuildPackageWeight(ArrayList sfItems, bool sfBackorder)
        {
            decimal weight = 0;
            foreach (COrderItem sfItem in sfItems)
            {
                long backorderQuantity = sfItem.SavedBOQty;
                long orderQuantity = sfItem.Quantity - backorderQuantity;

                if (sfBackorder && backorderQuantity <= 0) continue;
                if (!sfBackorder && orderQuantity <= 0) continue;

                decimal itemWeight = sfItem.Weight + sfItem.AttributesWeight;
                weight += (sfBackorder ? backorderQuantity : orderQuantity) * itemWeight;
            }
            return Convert.ToDouble(weight);
        }

        private static string BuildPaymentDescription(COrder sfOrder)
        {
            string methodName = StoreFrontConfiguration.PaymentMethodAccess.GetPaymentMethodName(sfOrder.PaymentMethod);
            if (methodName.Equals("echeck", StringComparison.InvariantCultureIgnoreCase))
                return methodName;
            if (methodName.Equals("credit card", StringComparison.InvariantCultureIgnoreCase))
                return string.Concat(sfOrder.OrderPayment.CardType, " -", GetCreditCardNumber(sfOrder.OrderPayment));
            if (methodName.Equals("po", StringComparison.InvariantCultureIgnoreCase))
                return string.Concat("Purchase Order #", sfOrder.OrderPayment.PONumber);
            if (methodName.Equals("phonefax", StringComparison.InvariantCultureIgnoreCase))
                return "Phone/Fax";
            if (methodName.Equals("cod", StringComparison.InvariantCultureIgnoreCase))
                return "COD";
            return string.Empty;
        }

        private static string BuildShippingStatus(COrder sfOrder)
        {
            int totalShipments = 0;
            foreach (COrderAddress address in sfOrder.OrderAddresses)
            {
                if (address.HasNonBackOrderShippableProducts)
                    totalShipments++;
                if (address.HasBackOrderShippableProducts)
                    totalShipments++;
            }
            if (totalShipments == 0)
                return "Non Shipping";

            int pendingShipments = 0;
            using (CManagement management = new CManagement(sfOrder.UID))
            {
                foreach (COrderShipmentStatus status in management.get_OrderShipStatus(sfOrder.UID))
                {
                    if (status.HasShipmentPending)
                        pendingShipments++;
                    if (status.HasBOShipmentPending)
                        pendingShipments++;
                }
            }
            if (pendingShipments == 0)
                return "Shipped";

            return pendingShipments == totalShipments ? "Unshipped" : "Partially Shipped";
        }

        private static string GetCreditCardNumber(COrderPayment sfOrderPayment)
        {
            if (!string.IsNullOrEmpty(sfOrderPayment.LastFourDigits))
                return sfOrderPayment.LastFourDigits;
            if (!string.IsNullOrEmpty(sfOrderPayment.CreditCardNumber))
            {
                string number = GetDecryptedValue(sfOrderPayment.CreditCardNumber);
                return number.Length > 4 ? number.Substring(number.Length - 4) : number;
            }
            return string.Empty;
        }

        private static string GetDecryptedValue(string value)
        {
            if (StoreFrontConfiguration.ConvertedFrom3DES)
                return string.Empty;
            string[] values = value.Split(' ');
            if (values.Length < 17)
                return string.Empty;

            try
            {
                byte[] key = new byte[8];
                byte[] iv = new byte[8];
                for (int index = 0; index < 8; index++)
                {
                    key[index] = Convert.ToByte(values[index]);
                    iv[index] = Convert.ToByte(values[index + 8]);
                }
                byte[] encrypted = new byte[values.Length - 16];
                for (int index = 0; index <= encrypted.GetUpperBound(0); index++)
                    encrypted[index] = Convert.ToByte(values[index + 16]);

                using (CStoreFrontCrypto2 crypto = new CStoreFrontCrypto2(encrypted))
                {
                    crypto.Type = CryptoType.Decrypt;
                    return crypto.GetData(key, iv);
                }
            }
            catch
            {
                return string.Empty;
            }
        }

        private static string BuildPaymentStatus(COrder sfOrder)
        {
            if (sfOrder.PaymentsPending || sfOrder.BOPaymentsPending)
                return PAYMENT_STATUS_PENDING;
            return PAYMENT_STATUS_PAID;
        }

        private static string BuildRateProviderName(string carrierCode)
        {
            if (string.IsNullOrEmpty(carrierCode) || carrierCode.Equals("NONE", StringComparison.InvariantCultureIgnoreCase))
                return null;

            using (CShippingManagement management = new CShippingManagement())
            using (DataSet carriers = management.getCarriers())
            {
                foreach (DataRow row in carriers.Tables[0].Rows)
                {
                    if (ShippingAgentHelper.ConvertToString(row["Code"]).Equals(carrierCode, StringComparison.CurrentCultureIgnoreCase))
                    {
                        return ShippingAgentHelper.ConvertToString(row["Name"]);
                    }
                }
            }
            return carrierCode;
        }

        private static DateTime? BuildShipDate(COrder sfOrder, COrderAddress sfOrderAddress, bool sfBackorder)
        {
            using (CManagement management = new CManagement(sfOrder.UID))
            {
                foreach (COrderShipmentStatus status in management.get_OrderShipStatus(sfOrder.UID))
                {
                    if (status.AddressID != sfOrderAddress.Address.ID)
                        continue;
                    string sentdate = management.get_Tracking(sfOrderAddress.Address.ID, sfBackorder ? 1 : 0).SentDate;
                    if (string.IsNullOrEmpty(sentdate))
                        return null;
                    DateTime outdate;
                    if (DateTime.TryParse(sentdate, out outdate))
                        return outdate;
                    return null;
                }
            }
            return null;
        }

        private static bool BuildShipped(COrder sfOrder, COrderAddress sfOrderAddress, bool sfBackorder)
        {
            using (CManagement management = new CManagement(sfOrder.UID))
            {
                foreach (COrderShipmentStatus status in management.get_OrderShipStatus(sfOrder.UID))
                {
                    if (status.AddressID != sfOrderAddress.Address.ID)
                        continue;
                    if (sfBackorder && !status.HasBOShipmentPending)
                        return true;
                    if (!sfBackorder && !status.HasShipmentPending)
                        return true;
                }
            }
            return false;
        }

        private static ShippingAddress BuildShippingAddress(StoreFront.SystemBase.Address sfAddress, bool residential)
        {
            ShippingAddress shippingAddress = new ShippingAddress();
            shippingAddress.AddressId = sfAddress.ID.ToString();
            shippingAddress.City = ShippingAgentHelper.StringOrNull(sfAddress.City);
            shippingAddress.Company = ShippingAgentHelper.StringOrNull(sfAddress.Company);
            shippingAddress.Country = ShippingAgentHelper.StringOrNull(sfAddress.Country);
            shippingAddress.Email = ShippingAgentHelper.StringOrNull(sfAddress.EMail);
            shippingAddress.Fax = ShippingAgentHelper.StringOrNull(sfAddress.Fax);
            shippingAddress.FirstName = ShippingAgentHelper.StringOrNull(sfAddress.FirstName);
            shippingAddress.LastName = ShippingAgentHelper.StringOrNull(sfAddress.LastName);
            shippingAddress.Line1 = ShippingAgentHelper.StringOrNull(sfAddress.Address1);
            shippingAddress.Line2 = ShippingAgentHelper.StringOrNull(sfAddress.Address2);
            shippingAddress.Phone = ShippingAgentHelper.StringOrNull(sfAddress.Phone);
            shippingAddress.PostalCode = ShippingAgentHelper.StringOrNull(sfAddress.Zip);
            shippingAddress.Region = ShippingAgentHelper.StringOrNull(sfAddress.State);
            shippingAddress.Residential = residential;
            return shippingAddress;
        }

        private static string BuildTrackingNumber(COrderAddress sfOrderAddress, bool sfBackorder)
        {
            using (COrderTrackingAccess access = new COrderTrackingAccess())
            using (DataSet tracking = access.GetTracking(sfOrderAddress.Address.ID))
            {
                if (tracking.Tables.Count == 0)
                    throw new ApplicationException("Invalid package ID.");

                DataRow[] sfTrack = tracking.Tables[0].Select("(VendorName='' OR VendorName IS NULL) AND BackOrderFlag = " + (sfBackorder ? 1 : 0));
                foreach (DataRow row in sfTrack)
                {
                    return row.IsNull("TrackingNumber") ? null : row["TrackingNumber"].ToString();
                }
            }
            return null;
        }

        private static UpdateResult BuildUpdateResult(Update update, COrder sfOrder, COrderAddress sfOrderAddress, bool sfBackorder)
        {
            try
            {
                List<string> notes = new List<string>();

                if (update.Destination != null)
                {
                    if (!AddressesAreEqual(sfOrderAddress, update.Destination))
                    {
                        using (COrderAddressAccess access = new COrderAddressAccess())
                        using (DataSet addresses = access.GetOrderAddress(sfOrder.UID))
                        {
                            if (addresses.Tables.Count == 0)
                                throw new ApplicationException("Invalid order ID.");

                            DataRow[] sfAddress = addresses.Tables[0].Select("UID=" + sfOrderAddress.Address.ID);
                            if (sfAddress.Length == 0)
                                throw new ApplicationException("Invalid package ID.");

                            sfAddress[0]["Address1"] = update.Destination.Line1;
                            sfAddress[0]["Address2"] = update.Destination.Line2;
                            sfAddress[0]["City"] = update.Destination.City;
                            sfAddress[0]["Company"] = update.Destination.Company;
                            sfAddress[0]["Country"] = update.Destination.Country;
                            sfAddress[0]["Fax"] = update.Destination.Fax;
                            sfAddress[0]["FirstName"] = update.Destination.FirstName;
                            sfAddress[0]["LastName"] = update.Destination.LastName;
                            sfAddress[0]["Phone"] = update.Destination.Phone;
                            sfAddress[0]["State"] = update.Destination.Region;
                            sfAddress[0]["Zip"] = update.Destination.PostalCode;
                            sfAddress[0]["Email"] = update.Destination.Email;
                            access.Update(addresses, access.TableName);
                        }
                        notes.Add("Changed shipping address.");
                    }
                }

                if (update.TrackingNumber != null)
                {
                    string trackingNumber = BuildTrackingNumber(sfOrderAddress, sfBackorder) ?? string.Empty;
                    if (!trackingNumber.Equals(update.TrackingNumber, StringComparison.InvariantCultureIgnoreCase))
                    {
                        using (COrderTrackingAccess access = new COrderTrackingAccess())
                        using (DataSet tracking = access.GetTracking(sfOrderAddress.Address.ID))
                        {
                            if (tracking.Tables.Count == 0)
                                throw new ApplicationException("Invalid package ID.");

                            DataRow[] sfTrack = tracking.Tables[0].Select("(VendorName='' OR VendorName IS NULL) AND BackOrderFlag=" + (sfBackorder ? 1 : 0));
                            if (sfTrack.Length == 0)
                            {
                                sfTrack = new DataRow[] { tracking.Tables[0].NewRow() };
                                sfTrack[0]["BackOrderFlag"] = sfBackorder;
                                sfTrack[0]["ShipToAddressID"] = sfOrderAddress.Address.ID;
                                sfTrack[0]["VendorName"] = string.Empty;
                                tracking.Tables[0].Rows.Add(sfTrack[0]);
                            }
                            sfTrack[0]["TrackingNumber"] = update.TrackingNumber;
                            access.Update(tracking, access.TableName);
                            notes.Add(string.Format("Changed shipment tracking number to {0}.", update.TrackingNumber));
                        }
                    }
                }

                if (update.TrackingProviderId != null)
                {
                    if (!sfOrderAddress.Address.ShipCarrierCode.Equals(update.TrackingProviderId, StringComparison.InvariantCultureIgnoreCase))
                    {
                        using (COrderAddressAccess access = new COrderAddressAccess())
                        using (DataSet addresses = access.GetOrderAddress(sfOrder.UID))
                        {
                            if (addresses.Tables.Count == 0)
                                throw new ApplicationException("Invalid order ID.");

                            DataRow[] sfAddress = addresses.Tables[0].Select("UID=" + sfOrderAddress.Address.ID);
                            if (sfAddress.Length == 0)
                                throw new ApplicationException("Invalid package ID.");

                            sfAddress[0]["ShipCarrierCode"] = update.TrackingProviderId;
                            access.Update(addresses, access.TableName);
                        }
                        notes.Add(string.Format("Changed shipment tracking provider code to {0}.", update.TrackingProviderId));
                    }
                }

                if (update.ShipDateSpecified)
                {
                    DateTime? shipdate = BuildShipDate(sfOrder, sfOrderAddress, sfBackorder);
                    if (!shipdate.HasValue || update.ShipDate != shipdate.Value)
                    {
                        using (COrderAddressAccess access = new COrderAddressAccess())
                        using (DataSet addresses = access.GetOrderAddress(sfOrder.UID))
                        {
                            if (addresses.Tables.Count == 0)
                                throw new ApplicationException("Invalid order ID.");

                            DataRow[] sfAddress = addresses.Tables[0].Select("UID=" + sfOrderAddress.Address.ID);
                            if (sfAddress.Length == 0)
                                throw new ApplicationException("Invalid package ID.");

                            if (sfBackorder)
                                sfAddress[0]["BOPending"] = 0;
                            else
                                sfAddress[0]["Pending"] = 0;
                            access.Update(addresses, access.TableName);
                        }

                        using (COrderTrackingAccess access = new COrderTrackingAccess())
                        using (DataSet tracking = access.GetTracking(sfOrderAddress.Address.ID))
                        {
                            if (tracking.Tables.Count == 0)
                                throw new ApplicationException("Invalid package ID.");

                            DataRow[] sfTrack = tracking.Tables[0].Select("(VendorName='' OR VendorName IS NULL) AND BackOrderFlag=" + (sfBackorder ? 1 : 0));
                            if (sfTrack.Length == 0)
                            {
                                sfTrack = new DataRow[] { tracking.Tables[0].NewRow() };
                                sfTrack[0]["BackOrderFlag"] = sfBackorder;
                                sfTrack[0]["ShipToAddressID"] = sfOrderAddress.Address.ID;
                                sfTrack[0]["VendorName"] = string.Empty;
                                tracking.Tables[0].Rows.Add(sfTrack[0]);
                            }
                            sfTrack[0]["SentDate"] = update.ShipDate.ToString("M/d/yyyy");
                            access.Update(tracking, access.TableName);
                        }
                        notes.Add(string.Format("Changed shipping date to {0}.", update.ShipDate.ToString("M/d/yyyy")));
                    }
                }

                if (notes.Count > 0)
                {
                    UpdateResult result = new UpdateResult();
                    result.OrderResult = BuildOrderResult(sfOrder.OrderNumber);
                    result.PackageResult = BuildPackageResult(sfOrder, sfOrderAddress, sfBackorder);
                    result.Message = string.Join(" ", notes.ToArray());
                    result.Success = true;
                    return result;
                }

                UpdateResult nochange = new UpdateResult();
                nochange.Success = true;
                nochange.Message = "No change.";
                return nochange;
            }
            catch (ApplicationException ex)
            {
                UpdateResult result = new UpdateResult();
                result.Success = false;
                result.Message = ex.Message;
                return result;
            }
            catch (Exception ex)
            {
                UpdateResult result = new UpdateResult();
                result.Success = false;
                result.Message = ex.ToString();
                return result;
            }
        }

        private static Supplier BuildVendor(IProduct sfItem)
        {
            try
            {
                using (CProductManagement product = new CProductManagement(Convert.ToInt32(sfItem.ProductID)))
                using (CVendor vendor = new CVendor(product.Vendor))
                {
                    if (vendor.VendorAddress.Company.Equals("no vendor", StringComparison.CurrentCultureIgnoreCase))
                        return null;

                    Supplier supplier = new Supplier();
                    supplier.Email = ShippingAgentHelper.StringOrNull(vendor.VendorAddress.EMail);
                    supplier.Name = vendor.VendorAddress.Company;
                    supplier.SupplierId = product.Vendor.ToString();
                    return supplier;
                }
            }
            catch (IndexOutOfRangeException)
            {
                // Product not in database
                return null;
            }
        }

        private static Address BuildWarehouse()
        {
            using (CShippingManagement management = new CShippingManagement())
            {
                Address address = new Address();
                address.AddressId = "Origin";
                address.City = ShippingAgentHelper.StringOrNull(management.AdminShipping.OriginCity);
                address.Company = ShippingAgentHelper.StringOrNull(management.AdminShipping.StoreName);
                address.Country = ShippingAgentHelper.StringOrNull(management.AdminShipping.OriginCountry);
                address.Email = ShippingAgentHelper.StringOrNull(management.AdminShipping.PrimaryEmail);
                address.PostalCode = ShippingAgentHelper.StringOrNull(management.AdminShipping.OriginZip);
                address.Region = ShippingAgentHelper.StringOrNull(management.AdminShipping.OriginState);
                return address;
            }
        }
    }
}